/*
 * Vulnerability detector: Debian EVR version matching functions
 * Piece of code extracted from Debian DPKG and RPM rpmLib source code.
 * Copyright (C) 2015-2020, Wazuh Inc.
 * February 20th, 2020.
 *
 * This program is free software; you can redistribute it
 * and/or modify it under the terms of the GNU General Public
 * License (version 2) as published by the FSF - Free Software
 * Foundation.
 */

#include "../wmodules.h"
#include "wm_vuln_detector_evr.h"

#ifdef UNIT_TESTING
/* Remove static qualifier when testing */
#define static
#endif

#ifndef WIN32

#undef S
#define S C_CTYPE_SPACE
#undef W
#define W C_CTYPE_WHITE
#undef B
#define B C_CTYPE_BLANK
#undef U
#define U C_CTYPE_UPPER
#undef L
#define L C_CTYPE_LOWER
#undef D
#define D C_CTYPE_DIGIT

static int (*comparator) (const char *, const char *, int);

static unsigned short int c_ctype[256] = {
/** 0 **/
    /* \0 */ 0,
    0,
    0,
    0,
    0,
    0,
    0,
    /* \a */ 0,
    /* \b */ 0,
    /* \t */ S | W | B,
    /* \n */ S | W,
    /* \v */ S,
    /* \f */ S,
    /* \r */ S,
    0,
    0,
/** 16 **/
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
/** 32 **/
    /*   */ S | W | B,
    /* ! */ 0,
    /* " */ 0,
    /* # */ 0,
    /* $ */ 0,
    /* % */ 0,
    /* & */ 0,
    /* ' */ 0,
    /* ( */ 0,
    /* ) */ 0,
    /* * */ 0,
    /* + */ 0,
    /* , */ 0,
    /* - */ 0,
    /* . */ 0,
    /* / */ 0,
/** 48 **/
    /* 0 */ D,
    /* 1 */ D,
    /* 2 */ D,
    /* 3 */ D,
    /* 4 */ D,
    /* 5 */ D,
    /* 6 */ D,
    /* 7 */ D,
    /* 8 */ D,
    /* 9 */ D,
    /* : */ 0,
    /* ; */ 0,
    /* < */ 0,
    /* = */ 0,
    /* > */ 0,
    /* ? */ 0,
/* 64 */
    /* @ */ 0,
    /* A */ U,
    /* B */ U,
    /* C */ U,
    /* D */ U,
    /* E */ U,
    /* F */ U,
    /* G */ U,
    /* H */ U,
    /* I */ U,
    /* J */ U,
    /* K */ U,
    /* L */ U,
    /* M */ U,
    /* N */ U,
    /* O */ U,
/* 80 */
    /* P */ U,
    /* Q */ U,
    /* R */ U,
    /* S */ U,
    /* T */ U,
    /* U */ U,
    /* V */ U,
    /* W */ U,
    /* X */ U,
    /* Y */ U,
    /* Z */ U,
    /* [ */ 0,
    /* \ */ 0,
    /* ] */ 0,
    /* ^ */ 0,
    /* _ */ 0,
/* 96 */
    /* ` */ 0,
    /* a */ L,
    /* b */ L,
    /* c */ L,
    /* d */ L,
    /* e */ L,
    /* f */ L,
    /* g */ L,
    /* h */ L,
    /* i */ L,
    /* j */ L,
    /* k */ L,
    /* l */ L,
    /* m */ L,
    /* n */ L,
    /* o */ L,
/* 112 */
    /* p */ L,
    /* q */ L,
    /* r */ L,
    /* s */ L,
    /* t */ L,
    /* u */ L,
    /* v */ L,
    /* w */ L,
    /* x */ L,
    /* y */ L,
    /* z */ L,
    /* { */ 0,
    /* | */ 0,
    /* } */ 0,
    /* ~ */ 0,
    0,
/* 128 */
};

/**
 * Check if the character is bits ctype.
 */
bool c_isbits(int c, enum c_ctype_bit bits)
{
    return ((c_ctype[(unsigned char)c] & bits) != 0);
}
/**
 * Check if the character is [0-9].
 */
bool c_isdigit(int c)
{
    return c_isbits(c, C_CTYPE_DIGIT);
}

/**
 * Check if the character is [a-zA-Z].
 */
bool c_isalpha(int c)
{
    return c_isbits(c, C_CTYPE_ALPHA);
}
/**
 * Give a weight to the character to order in the version comparison.
 *
 * @param c An ASCII character.
 */
int order(int c)
{
    if (c_isdigit(c))
        return 0;
    else if (c_isalpha(c))
        return c;
    else if (c == '~')
        return -1;
    else if (c)
        return c + 256;
    else
        return 0;
}

/*
* Compare alpha and numeric segments of two DEB versions
* Extracted from DPKG source code.
* return > 0: a is newer than b
*          0: a and b are the same version
*        < 0: b is newer than a
*/
static int deb_verrevcmp(const char *a, const char *b, __attribute__((unused)) int revision)
{
    if (a == NULL)
        a = "";
    if (b == NULL)
        b = "";

    while (*a || *b) {
        int first_diff = 0;

        while ((*a && !c_isdigit(*a)) || (*b && !c_isdigit(*b))) {
            int ac = order(*a);
            int bc = order(*b);

            if (ac != bc)
                return ac - bc;

            a++;
            b++;
        }
        while (*a == '0')
            a++;
        while (*b == '0')
            b++;
        while (c_isdigit(*a) && c_isdigit(*b)) {
            if (!first_diff)
                first_diff = *a - *b;
            a++;
            b++;
        }

        if (c_isdigit(*a))
            return 1;
        if (c_isdigit(*b))
            return -1;
        if (first_diff)
            return first_diff;
    }

    return 0;
}

/*
* Compare alpha and numeric segments of two RPM versions
* Extracted from rpmLib source code.
* return 1: a is newer than b
*        0: a and b are the same version
*       -1: b is newer than a
*/
static int rpm_vercmp(const char * a, const char * b, __attribute__((unused)) int revision)
{
    /* easy comparison to see if versions are identical */
    if (!strcmp(a, b)) return 0;

    char oldch1, oldch2;
    char abuf[strlen(a)+1], bbuf[strlen(b)+1];
    char *str1 = abuf, *str2 = bbuf;
    char * one, * two;
    int rc;
    int isnum;

    strcpy(str1, a);
    strcpy(str2, b);

    one = str1;
    two = str2;

    /* loop through each version segment of str1 and str2 and compare them */
    while (*one || *two) {
        while (*one && !(c_isalpha(*one) || c_isdigit(*one)) && *one != '~' && *one != '^') one++;
        while (*two && !(c_isalpha(*two) || c_isdigit(*two)) && *two != '~' && *two != '^') two++;

        /* handle the tilde separator, it sorts before everything else */
        if (*one == '~' || *two == '~') {
            if (*one != '~') return 1;
            if (*two != '~') return -1;
            one++;
            two++;
            continue;
        }

        /*
        * Handle caret separator. Concept is the same as tilde,
        * except that if one of the strings ends (base version),
        * the other is considered as higher version.
        */
        if (*one == '^' || *two == '^') {
            if (!*one) return -1;
            if (!*two) return 1;
            if (*one != '^') return 1;
            if (*two != '^') return -1;
            one++;
            two++;
            continue;
        }

        /* If we ran to the end of either, we are finished with the loop */
        if (!(*one && *two)) break;

        str1 = one;
        str2 = two;

        /* grab first completely alpha or completely numeric segment */
        /* leave one and two pointing to the start of the alpha or numeric */
        /* segment and walk str1 and str2 to end of segment */
        if (c_isdigit(*str1)) {
            while (*str1 && c_isdigit(*str1)) str1++;
            while (*str2 && c_isdigit(*str2)) str2++;
            isnum = 1;
        } else {
            while (*str1 && c_isalpha(*str1)) str1++;
            while (*str2 && c_isalpha(*str2)) str2++;
            isnum = 0;
        }

        /* save character at the end of the alpha or numeric segment */
        /* so that they can be restored after the comparison */
        oldch1 = *str1;
        *str1 = '\0';
        oldch2 = *str2;
        *str2 = '\0';

        /* this cannot happen, as we previously tested to make sure that */
        /* the first string has a non-null segment */
        if (one == str1) return -1;   /* arbitrary */

        /* take care of the case where the two version segments are */
        /* different types: one numeric, the other alpha (i.e. empty) */
        /* numeric segments are always newer than alpha segments */
        /* XXX See patch #60884 (and details) from bugzilla #50977. */
        if (two == str2) return (isnum ? 1 : -1);

        if (isnum) {
            size_t onelen, twolen;
            /* this used to be done by converting the digit segments */
            /* to ints using atoi() - it's changed because long  */
            /* digit segments can overflow an int - this should fix that. */

            /* throw away any leading zeros - it's a number, right? */
            while (*one == '0') one++;
            while (*two == '0') two++;

            /* whichever number has more digits wins */
            onelen = strlen(one);
            twolen = strlen(two);
            if (onelen > twolen) return 1;
            if (twolen > onelen) return -1;
        }

        /* strcmp will return which one is greater - even if the two */
        /* segments are alpha or if they are numeric.  don't return  */
        /* if they are equal because there might be more segments to */
        /* compare */
        rc = strcmp(one, two);
        if (rc) return (rc < 1 ? -1 : 1);

        /* restore character that was replaced by null above */
        *str1 = oldch1;
        one = str1;
        *str2 = oldch2;
        two = str2;
    }

    /* this catches the case where all numeric and alpha segments have */
    /* compared identically but the segment sepparating characters were */
    /* different */
    if ((!*one) && (!*two)) return 0;

    /* whichever version still has characters left over wins */
    if (!*one) return -1; else return 1;
}

/*
* Compare alpha and numeric segments of two NVD versions
* return > 0: a is newer than b
*          0: a and b are the same version
*        < 0: b is newer than a
*/
static int nvd_verrevcmp(const char *a, const char *b, int revision)
{
    int result;
    char *rev;
    size_t rev_size;

    // In the NVD version comparison, we avoid the revision
    // comparison if any of them is not present. 
    if ((a == NULL) || (b == NULL)) {
        return 0;
    }

    // When comparing revisions, in some cases the NVD's is shorter because it's
    // more generic than the package's, but that doesn't mean they are different.
    // Example: Package version: 3.0pl1-128.1ubuntu1
    //          NVD version: 3.0pl1-128.
    // To avoid an incorrect comparison, we will only compare the number of
    // characters that the NVD version contains.
    if (revision) {
        if (strlen(a) > strlen(b)) {
            rev_size = strlen(b);
        } else {
            rev_size = strlen(a);
        }

        os_calloc(1, rev_size + 1, rev);
        strncpy(rev, a, rev_size);

        result = deb_verrevcmp(rev, b, 1);

        os_free(rev);
    } else {
        result = deb_verrevcmp(a, b, 0);
    }

    return result;
}

/**
 * Compares two packages versions.
 *
 * This function follows the convention of the comparator functions used by
 * qsort().
 *
 *
 * @param a The first version.
 * @param b The second version.
 *
 * @retval 0 If a and b are equal.
 * @retval <0 If a is smaller than b.
 * @retval >0 If a is greater than b.
 */
int pkg_version_compare(const struct pkg_version *a, const struct pkg_version *b)
{
    int rc;

    // We will compare the epoch only if both values are >= 0. If any of the values
    // is < 0, it means that we are comparing NVD versions and that the epoch is not
    // present. In this case we avoid the epoch comparison.
    if (a->epoch >= 0 && b->epoch >= 0)
    {
        if (a->epoch > b->epoch)
            return 1;
        if (a->epoch < b->epoch)
            return -1;
    }

    rc = comparator(a->version, b->version, 0);
    if (rc)
        return rc;

    return comparator(a->revision, b->revision, 1);
}

/**
 * EVR string comparator. It allows to compare two packages versions based on a
 * specific relation and by specifying the package version type (DEB, RPM, NVD).
 *
 * @param a The first version.
 * @param rel The relation.
 * @param b The second version.
 * @param type The package type of the versions being compared.
 *
 * @retval 1 If the expression “a rel b” is true.
 * @retval 1 If rel is PKG_RELATION_NONE or vertype is VER_TYPE_NONE
 * @retval 0 Otherwise.
 *
 */
bool pkg_version_relate(const struct pkg_version *a, enum pkg_relation rel, const struct pkg_version *b, version_type vertype)
{
    int rc;

    if (PKG_RELATION_NONE == rel || VER_TYPE_NONE == vertype)
        return true;

    // Assigning version comparator
    switch (vertype)
    {
    case VER_TYPE_DEB:
        comparator = deb_verrevcmp; break;
    case VER_TYPE_RPM:
        comparator = rpm_vercmp; break;
    case VER_TYPE_NVD:
        comparator = nvd_verrevcmp; break;
    default:
        mterror(WM_VULNDETECTOR_LOGTAG, "unknown version_type %d", vertype);
        return false;
    }

    rc = pkg_version_compare(a, b);

    switch (rel) {
    case PKG_RELATION_EQ:
        return rc == 0;
    case PKG_RELATION_LT:
        return rc < 0;
    case PKG_RELATION_LE:
        return rc <= 0;
    case PKG_RELATION_GT:
        return rc > 0;
    case PKG_RELATION_GE:
        return rc >= 0;
    default:
        mterror(WM_VULNDETECTOR_LOGTAG, "unknown pkg_relation %d", rel);
    }
    return false;
}

#endif
